package gotnames.web;

import gotnames.Utils;
import gotnames.dm.KTrans;
import gotnames.dm.QueryBuilder;
import gotnames.dm.User;
import gotnames.web.Facebook.FbNotAuthorizedException;
import gotnames.web.st.GotNamesTask;

import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import com.medallia.spider.SpiderServlet;
import com.medallia.tiny.Clock;
import com.medallia.tiny.Empty;
import com.medallia.tiny.ObjectProvider;
import com.medallia.tiny.ObjectProvider.ObjectFactory;

/**
 * Extension of {@link SpiderServlet} for Got Names.
 * {@link #registerObjects(ObjectProvider, RequestHandler)} is overridden to
 * register the objects available to tasks.
 * <p>
 * The class {@link GotNamesTask} is the parent class of most tasks in Got Names
 * (except for tasks where the user is not yet logged in).
 */
public class GotNamesServlet extends SpiderServlet {
	private static final Logger LOG = Utils.getLog();
	
	private static final String FACEBOOK_DEV_APP_ID = "121767821223439";
	private static final String FACEBOOK_PROD_APP_ID = "183954231614743";
	
	private String serverInfo;
	private boolean development;
	
	private static final PersistenceManagerFactory PM_FACTORY = JDOHelper.getPersistenceManagerFactory("transactions-optional");
	
	@Override public void init(ServletConfig cfg) throws ServletException {
		super.init(cfg);
		serverInfo = getServletContext().getServerInfo();
		development = serverInfo.contains("Development");
		setDebugMode(development);
	}
	
	/** Thrown if a {@link User} object is requested by a task, but the user is not authenticated */
	private static class NoAuthException extends RuntimeException {
		
	}
	
	@Override protected void handleException(HttpServletRequest req, HttpServletResponse res, Throwable t) throws IOException {
		if (t instanceof NoAuthException) {
			res.sendRedirect("/" + getDefaultURI());
		} else if (t instanceof FbNotAuthorizedException) {
			res.sendRedirect("/facebookProfilePic");
		} else {
			LOG.severe("Unexpected exception: " + t.getMessage());
			super.handleException(req, res, t);
		}
	}
	
	/** Object which handles authentication of a {@link User} */
	public interface UserAuthenticator {
		/**
		 * @return true if the user with the given token was successfully
		 *         authenticated and the web session has been updated
		 */
		boolean authenticate(String token);

		/**
		 * Method which should be called if the {@link User} object of the
		 * currently logged in user is changed since it is cached in the session
		 */
		void userUpdated(User user);
		/**
		 * Logoff the currently logged on user.
		 */
		void logout();
	}
	
	private static final String USER_SESSION_KEY = User.class.getCanonicalName();

	/**
	 * The available objects to tasks are:
	 * <ul>
	 * 
	 * <li> {@link PersistenceManager}
	 * <li> {@link User} (if the user is logged in, otherwise redirects to the start page)
	 * <li> {@link UserAuthenticator}
	 * <li> {@link Facebook}
	 * </ul>
	 */
	@Override protected void registerObjects(final ObjectProvider injector, final RequestHandler rh) {
		injector
			.registerFactory(new ObjectFactory<PersistenceManager>() {
				@Override public PersistenceManager make() {
					PersistenceManager pm = PM_FACTORY.getPersistenceManager();
					return pm;
				}
			})
			.registerFactory(new ObjectFactory<User>() {
				@Override public User make() {
					HttpSession session = rh.getSession();
					
					// check if there is an auth token
					String authToken = rh.getInput("t", String.class);
					User user;
					if (authToken != null) {
						PersistenceManager pm = PM_FACTORY.getPersistenceManager();
						user = attemptLogon(pm, session, authToken);
					} else {
						user = (User) session.getAttribute(USER_SESSION_KEY);
					}
					
					if (user == null)
						throw new NoAuthException();
					return user;
				}
			})
			.register(new UserAuthenticator() {
				@Override public boolean authenticate(String token) {
					User user = attemptLogon(PM_FACTORY.getPersistenceManager(), rh.getSession(), token);
					return user != null;
				}
				@Override public void userUpdated(User user) {
					rh.getSession().setAttribute(USER_SESSION_KEY, user);
				}
				@Override public void logout() {
					rh.getSession().removeAttribute(USER_SESSION_KEY);
				}
			})
			.registerFactory(new ObjectFactory<Facebook>() {
				@Override public Facebook make() {
					String appId = development ? FACEBOOK_DEV_APP_ID : FACEBOOK_PROD_APP_ID;
					final String cookieName = "fbs_" + appId;
					
					return new Facebook(appId, getAccessToken(cookieName, rh)) {
						@Override public void logout() {
							rh.removeCookieValue(cookieName);
							rh.getSession().invalidate();
						}
					};
				}
				private String getAccessToken(String cookieName, RequestHandler rh) {
					String str = rh.getCookieValue(cookieName);
					if (str == null)
						return null;
					
					Map<String, String> cookies = Empty.hashMap();
					for (String cv : str.split("&")) {
						String[] kv = cv.split("=");
						if (kv.length == 2)
							cookies.put(kv[0], kv[1]);
					}
					
					return cookies.get("access_token");
				}
			})
		;
	}
	
	private User attemptLogon(PersistenceManager pm, HttpSession session, String authToken) {
		final User user = QueryBuilder.begin(pm, User.class).getSingleByField("authToken", authToken);
		if (user == null)
			return null;
		
		new KTrans.Void(pm) {
			@Override protected void run() {
				user.setLastLogin(Clock.now());
				pm.makePersistent(user);
			}
		}.go();
		
		session.setAttribute(USER_SESSION_KEY, user);
		
		return user;
	}

	@Override protected String getDefaultURI() {
		return "register";
	}
	
}
